package com.lmq.backend.consumer.utils;

import com.alibaba.fastjson.JSONObject;
import com.lmq.backend.consumer.WebSocketServer;
import com.lmq.backend.pojo.Bot;
import com.lmq.backend.pojo.Record;
import com.lmq.backend.pojo.User;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Random;
import java.util.concurrent.locks.ReentrantLock;

public class Game extends Thread {
    private final Integer rows;
    private final Integer cols;
    private final Integer innerWallsCount;

    private final int[][] g;

    private final Player playerA;
    private final Player playerB;

    private ReentrantLock mtx = new ReentrantLock();

    private final static int dx[] = {-1, 0, 1, 0};
    private final static int dy[] = {0, 1, 0, -1};

    private Integer nextStepA = null;
    private Integer nextStepB = null;

    private String status = "playing";

    private String loser = "";

    private Integer cnt = 0;

    private static final String addBotUrl = "http://127.0.0.1:8997/bot/add/";

    public Game(Integer rows, Integer cols, Integer innerWallsCount, Integer idA, Bot botA, Integer idB, Bot botB) {
        this.rows = rows;
        this.cols = cols;
        this.innerWallsCount = innerWallsCount;
        this.g = new int[rows][cols];

        Integer botIdA = -1;
        Integer botIdB = -1;
        String botCodeA = "";
        String botCodeB = "";


        if(botA != null) {
            botIdA = botA.getId();
            botCodeA = botA.getContent();
        }

        if(botB != null) {
            botIdB = botB.getId();
            botCodeB = botB.getContent();
        }

        this.playerA = new Player(idA, botIdA, botCodeA, rows - 2, 1, new ArrayList<>());
        this.playerB = new Player(idB, botIdB, botCodeB, 1, cols - 2, new ArrayList<>());
    }

    public void setNextStepA(Integer nextStepA) {
        mtx.lock();
        try {
            this.nextStepA = nextStepA;
        } finally {
            mtx.unlock();
        }
    }

    public void setNextStepB(Integer nextStepB) {
        mtx.lock();
        try {
            this.nextStepB = nextStepB;
        } finally {
            mtx.unlock();
        }
    }

    public Player getPlayerA() {
        return this.playerA;
    }

    public Player getPlayerB() {
        return this.playerB;
    }

    public int[][] getG() {
        return this.g;
    }

    private boolean checkMap(int sx, int sy, int tx, int ty) {
        if(sx == tx && sy == ty) return true;
        g[sx][sy] = 1;
        for(int i = 0; i < 4; i++) {
            int x = sx + dx[i], y = sy + dy[i];
            if(x >= 0 && x < this.rows && y >= 0 && y < this.cols && g[x][y] == 0) {
                if(checkMap(x, y, tx, ty)) {
                    g[sx][sy] = 0;
                    return true;
                }
            }
        }
        g[sx][sy] = 0;
        return false;
    }

    private boolean draw() {
        for(int i = 0; i < this.rows; i++) {
            for(int j = 0; j < this.cols; j++) {
                g[i][j] = 0;
            }
        }

        for(int i = 0; i < this.rows; i++) {
            g[i][0] = g[i][this.cols - 1] = 1;
        }

        for(int i = 0; i < this.cols; i++) {
            g[0][i] = g[this.rows - 1][i] = 1;
        }

        Random random = new Random();
        for(int i = 0; i < this.innerWallsCount / 2; i++) {
            for(int j = 0; j < 1000; j++) {
                int r = random.nextInt(this.rows - 2) + 1;
                int c = random.nextInt(this.cols - 2) + 1;

                if(g[r][c] == 1 || g[this.rows - 1 - r][this.cols - 1 - c] == 1) {
                    continue;
                }

                if(r == this.rows - 2 && c == 1 || r == 1 && c == this.cols - 2) {
                    continue;
                }

                g[r][c] = g[this.rows - 1 -r][this.cols - 1 - c] = 1;
                break;
            }
        }
        return checkMap(this.rows - 2, 1, 1, this.cols - 2);
    }

    public void createMap() {
        for(int i = 0; i < 1000; i++) {
            if(draw()) {
                break;
            }
        }
    }

    void sendBotCode(Player player) {
        cnt++;
        if(cnt <= 2) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }

        if(player.getBotId().equals(-1)) {
            return;
        }

        Player me, you;
        if(player.getId().equals(playerA.getId())) {
            me = playerA;
            you = playerB;
        }else {
            me = playerB;
            you = playerA;
        }

        MultiValueMap<String, String> data = new LinkedMultiValueMap<>();
        JSONObject msg = new JSONObject();
        msg.put("map", this.mapString());
        msg.put("me_sx", me.getSx());
        msg.put("me_sy", me.getSy());
        msg.put("me_steps", me.getStepsString());
        msg.put("you_sx", you.getSx());
        msg.put("you_sy", you.getSy());
        msg.put("you_steps", you.getStepsString());

        data.add("userId", player.getId().toString());
        data.add("botCode", player.getBotCode());
        data.add("input", msg.toJSONString());

        WebSocketServer.restTemplate.postForObject(addBotUrl, data, String.class);
    }

    private boolean nextStep() {
        try {
            Thread.sleep(350);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        sendBotCode(playerA);
        sendBotCode(playerB);

        for(int i = 0; i < 100; i++) {
            try {
                Thread.sleep(100);
                mtx.lock();
                try {
                    if(nextStepA != null && nextStepB != null) {
                        playerA.getSteps().add(nextStepA);
                        playerB.getSteps().add(nextStepB);
                        return true;
                    }
                } finally {
                    mtx.unlock();
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        return false;
    }

    private void sendAllMessage(String message) {
        System.out.println(message);
        if(WebSocketServer.users.get(playerA.getId()) != null) {
            WebSocketServer.users.get(playerA.getId()).sendMessage(message);
        }

        if(WebSocketServer.users.get(playerB.getId()) != null) {
            WebSocketServer.users.get(playerB.getId()).sendMessage(message);
        }
    }

    private boolean check_valid(List<Cell> cellsA, List<Cell> cellsB) {
        int n = cellsA.size();
        Cell cell = cellsA.get(n - 1);
        if(g[cell.x][cell.y] == 1) return false;

        for(int i = 0; i < n - 1; i++) {
            if(cellsA.get(i).getX() == cell.x && cellsA.get(i).getY() == cell.y) {
                return false;
            }
        }

        for(int i = 0; i < n; i++) {
            if(cellsB.get(i).getX() == cell.x && cellsB.get(i).getY() == cell.y) {
                return false;
            }
        }

        return true;
    }

    private void judge() {
        List<Cell> cellsA = playerA.getCells();
        List<Cell> cellsB = playerB.getCells();
        boolean validA = check_valid(cellsA, cellsB);
        boolean validB = check_valid(cellsB, cellsA);

        if(!validA || !validB) {
            this.status = "finished";
            if(!validA && !validB) {
                loser = "all";
            }else if(!validA) {
                loser = "A";
            }else {
                loser = "B";
            }
        }
    }

    private void sendMove() {
        mtx.lock();
        try {
            JSONObject resp = new JSONObject();
            resp.put("event", "move");
            resp.put("a_direction", nextStepA);
            resp.put("b_direction", nextStepB);
            sendAllMessage(resp.toJSONString());
            nextStepA = nextStepB = null;
        } finally {
            mtx.unlock();
        }
    }

    private String mapString() {
        StringBuilder sb = new StringBuilder();
        for(int i = 0; i < rows; i++) {
            for(int j = 0; j < cols; j++) {
                sb.append(g[i][j]);
            }
        }
        return sb.toString();
    }

    private void updateRating(Player player, Integer rating) {
        User user = WebSocketServer.userMapper.selectById(player.getId());
        user.setRating(rating);
        WebSocketServer.userMapper.updateById(user);
    }

    // Elo算法
    private Integer calculateRating(Integer ratingA, Integer ratingB, Integer isWin) {
        double expectScore = 1 / (1 + Math.pow(10, (ratingB - ratingA) / 400.0));
        // ΔR=K×(S−E)
        int k = 10;
        Integer delta = (int)Math.ceil(k * (isWin - expectScore));
        Integer res = ratingA + delta;
        return res;
    }
    private boolean saveDataBases() {

        Integer ratingA = WebSocketServer.userMapper.selectById(playerA.getId()).getRating();
        Integer ratingB = WebSocketServer.userMapper.selectById(playerB.getId()).getRating();

        Integer resA = ratingA, resB = ratingB;

        if("A".equals(loser)) {
            resA = calculateRating(ratingA, ratingB, 0);
            resB = calculateRating(ratingB, ratingA, 1);
        }else if("B".equals(loser)) {
            resA = calculateRating(ratingA, ratingB, 1);
            resB = calculateRating(ratingB, ratingA, 0);
        }else {
            if(resA < resB) resA += 1;
            else if(resA > resB) resB += 1;
        }

        updateRating(playerA, resA);
        updateRating(playerB, resB);

        Record record = new Record(null,
                getPlayerA().getId(),
                getPlayerA().getSx(),
                getPlayerA().getSy(),
                getPlayerB().getId(),
                getPlayerB().getSx(),
                getPlayerB().getSy(),
                getPlayerA().getStepsString(),
                getPlayerB().getStepsString(),
                mapString(),
                loser,
                new Date()
                );

        WebSocketServer.recordMapper.insert(record);
        return true;
    }

    private void sendResult() {
        JSONObject resp = new JSONObject();
        resp.put("event", "result");
        resp.put("loser", loser);
        saveDataBases();
        sendAllMessage(resp.toJSONString());
    }

    @Override
    public void run() {
        for(int i = 0; i < 1000; i++) {
            if(nextStep()) {
                judge();
                if(this.status.equals("playing")) {
                    sendMove();
                }else {
                    sendResult();
                    break;
                }
            }else {
                this.status = "finished";
                System.out.println("end of the game");
                mtx.lock();
                try {
                    if(nextStepA == null && nextStepB == null) {
                        loser = "all";
                    }else if(nextStepA == null) {
                        loser = "A";
                    }else {
                        loser = "B";
                    }
                } finally {
                    mtx.unlock();
                }
                sendResult();
                break;
            }
        }
    }
}
